実践 Svelte入門 エンジニア選書 でSvelteに入門する
Svelte自体はちょこちょこ触ろうとしてるけど、本来がバックエンドエンジニアなこともあって業務に使えていない
そのため少し目を離したらすぐに使い方を忘れてしまっている
一度しっかりおべんきょして、脳みそにちょっぴりでもいいから何か残るようにしたいなーってことで、
入門書を頭からなぞっていってみる
https://m.media-amazon.com/images/I/71EJQv99qEL.jpg
序文
Svelteコンパイラは、Svelteコンポーネントを差分計算総統の処理を終えたJavaScriptに変換します
実行時の差分計算が不要になるため、ブラウザ上で実行するJavaScriptの量が減り、通信料や性能、使用メモリ量も改善します
SvelteKitは、Svelteを利用したWebアプリケーションをより便利に構築するためのWebフレームワークです
はじめに
本書では、すべてのサンプルコードをJavaScriptで掲載します。
第1章 はじめてのSvelte
既に以下のJavaScriptの開発環境は構築してあることを前提とします
やべぇ!急いで環境構築だ!!
$ node --version
v18.16.1
古いなぁ、最新版のダウンロードからやっておく
$ node --version
v20.11.1
$ npm --version
10.2.4
IDEはIntelliJ IDEA Ultimateで行く予定。プラグインがあったと思う
あと基本的にはJavaScriptで掲載されているコードもTypeScriptで書くつもり
2章以降では、TypeScriptを使う場合のセットアップ方法や補足などを適宜解説しています
であれば、2章以降をTypeScriptで書くようにしようかな
1章が結構長いなら1章からTypeScriptにするくらいのつもりでいとく
$ npm install
$ npm run dev
run dev を忘れてなんやったっけな~になりがち
このように、Svelteの構文は普通にHTML/CSSに非常に近いものになっています
ボタンを押すたびに表示される内容が変わりますが、これを「状態を持つ」と言います
ステート、なるほど
{#if} ~ {:else} ~ {/if}
#ではじまり、:で繋がり、/で終わる違和感
cart.push(...)は確かにcart要素を追加しますが、それはSvelteからは認識されず、画面も書き変わりません
javascriptでも cart = [...cart, productId] と書けることを知らなかった
代入式があることが大事なのね
{#each 配列 as 変数} ~ {/each}
ifと同じように #から/で終わり
{#if !cart.includes(product.id)}
のように、式の中なら {product.id} のように書かなくていい
{product.id} のように書くのは、HTMLにそのまま表示
「コンポーネント」とは、UIを構成する部品の単位のことです
App.svelteから、商品の「画像に関する情報」をSlider.svelteに渡してもらう必要があります
ここで使うのがプロパティ(properties/props)です
プロパティはexport letという構文で記述します。Slider.svelteの戦闘に次のように記載しましたが、この場合は「imagesというプロパティを定義する」という意味になります
import文で.svelteファイルを指定すると、そのファイルに掛かれたコンポーネントをインポートできます
インポートしたコンポーネントは、まるでHTMLタグであるかのように<Slider>という記法で使えます
Sliderコンポーネントの内部状態であるはずのimagesですが、プロパティとして宣言されていることで、呼び出し側からその値を設定できるようになっています
プロパティの指定はHTMLの属性と同じようにprop={...}のように書きます
コンポーネントめっちゃ便利
パーツを別に出しておけば、似たような画面をいっぱい作るときは楽になる
数年前までは業務用のWebシステムでSPAなんて無駄ばっかりって感覚やったけど、Svelteみたいなコンパイラになってくるとコンポーネント指向は強い
ある状態を、依存関係にある別の状態に応じて更新するための仕組みが用意されています
$: という記号が先頭についていますが、これは依存している他の状態の変化に応じてleftIndexやrightIndexを書き換えることを意味しています
めっちゃ便利でびびった
計算量が少なくなることはないにしても、バグが混入する機会が減らせるのは強い
ただ、なんでここの値変わったの?が一瞬分からなくなるのが怖くはある
第2章 Svelteの基本
TypeScriptサポートを有効にする場合は、最後の --template svelte を --template svelte-ts に変えて実行します
$ npm create vite@latest svelte-book-playground -- --template svelte-ts
vite.config.jsにはViteの設定を記載します
svelteテンプレートを指定してセットアップした場合は、デフォルトでSvelte向けの設定が記載されているため、特に変更する必要はありません
UIをコンポーネントという部品に分割して開発できることが利点の1つです
コンポーネント内の関心毎だけに集中できるほか、同じコンポーネントを様々な箇所で再利用しやすくなります
Svelteのコンポーネントは.svelteファイルとして作成し、1つのファイル内にマークアップ、ロジック、スタイルをまとめて記述できます
<script>ブロックの記述はそのコンポーネントにのみ影響を与えます
<script>ブロックと同様に、<style>ブロックの記述はそのコンポーネントに飲み影響を与えます
<script>ブロックでTypeScriptを使用することもできます。使用する場合、ブロックの開始を<script>の代わりに<script lang="ts">とします
テンプレート内で {式} のように書くと、式の評価結果が表示されます
{}の中には任意のJavaScriptの指揮を書けます。注意点として、正規表現リテラルを書く場合はカッコ()で囲む必要があります
属性名と変数名が同じ場合、以下のような省略記法が使えます <a {href}>Svelte 公式サイト</a>
disabled, required等の論理属性の場合、{}内の式の評価結果が真値の場合にのみ属性が設定されます
{...object}のような記法を使うと、objectのキーと値をまとめて属性として設定できます。この機能を「スプレッド属性」と呼びます
JavaScriptのスプレッド構文と似ていますが、スプレッド属性はオブジェクトにのみ使え、配列には使えません
IntelliJでスプレッド属性を使ったらエラーが出た
ただ実際には問題なく処理できてそうなので、プラグイン側の問題な気がする
https://scrapbox.io/files/65ef548e32db6a00269ba23f.png
テンプレート内では、通常のHTMLと同様に<!-- -->で囲んだ部分がコメントとして扱われます
<script>や<style>ブロック内では、それぞれJavaScriptのコメント、CSSのコメントを使います
<style>ブロックに記述したスタイルは、この.svelteファイルにスコープ化されます
スコープ化を無効にして全体に適用したいスタイルがある場合、:global修飾子が使えます
class: クラス名 = {真偽値} のように書くと、真偽値が真値の場合にのみクラス名をclass属性に付加できます
変数名とクラス名が同じ場合は、さらに省略して <li class:active>トップ</li> のようにも書けます
<button class="button" class:blue={theme === 'blue'} class:disabled>送信<button>
style属性の省略記法が使えます
<div style:color="red">赤色で表示されます。</div>
<div style:color={color}>color変数に応じた色で表示されます。</div>
HTMLには、条件分岐や繰り返しなどをする記法はありませんが、Svelteのテンプレートにはそのための記法が備わっています
これらの記法は{#xxx}から始まって{/xxx}で終わる構造になっています
{#if 条件式} {:else} {/if}
{#if 条件式} {:else if 条件式} {else} {/if}
コンテンツの途中で現れても構いません。ただし、タグの中に書くことはできません
属性値などを動的に変えたい場合は、動的な属性値の気泡を使ってください
<button {#if !valid}disabled{/if}>送信</button> が動かない
<button disabled={!valid}>送信</button> で対応
{#each 配列 as 変数} {/each}
{#each} ~ {/each}の間に{:else}ブロックを書くと、配列が空の場合に{:else} ~ {/each}の内容が表示される
{#each 配列 as 変数, インデックス変数} {/each}
{#await}ブロックは、Promiseに対して使える便利なブロックです
{#await Promiseの値} {:then 変数} {:catch 変数} {/await}
省略形 {#await promise then message} {/await}
省略形 {#await promise catch error} {/await}
import文で.svelteファイルを読み込みます
読み込んだコンポーネントは、参照名をHTMLタグのように書くことで、そのコンポーネントの内容をその部分に表示できます
通常のHTMLタグと区別するために、参照名の先頭は大文字にする必要があります
コンポーネントを参照するときは、終了タグを省略することはできません
<SvelteLogo></SvelteLogo>と明記するか、省略形の<SvelteLogo />のように書きます
あるコンポーネントを使用するコンポーネントのことを「親コンポーネント」または「親」と呼びます
あるコンポーネントから使用されるコンポーネントを「子コンポーネント」または「子」と呼びます
子コンポーネント側であらかじめプロパティを作成しておけば、そのプロパティを買いして親コンポーネントから何らかの値を受け渡せるようになります
通常のJavaScriptでは、exportはそのファイル内の関数やオブジェクトなどを別のファイルから使えるようにするという意味
Svelteでは、この変数の値をコンポーネントの外から設定できるようにするという意味
TypeScriptでプロパティの型を指定している場合、型に合わない値をプロパティに渡すと、TypeScriptの型検査によってエラーになります
プロパティは文字列以外のオブジェクトも問題なく受渡できます
export let user: {name: string, id: string, bio: string}; TypeScriptではこのように型指定できる
{...object} のようなスプレッド属性の気泡を、プロパティに対して使用できます
これを使うと、複数のプロパティをオブジェクトとして一括で渡せるようになります
スプレッド属性の対になるものとして、$$props という特別な定義済み変数が存在します
$$props には、そのコンポーネントに渡されているすべてのプロパティがオブジェクトとして格納されています
Svelte コンポーネント内で、インポートせずとも使えます
$$propsを使う機会はあまりりません。なぜなら、プロパティはexport letで宣言した変数からアクセスできるからです
$$propsが役立つケースとして、自身が受け取ったプロパティを別のコンポーネントに転送したい場合があります
$$propsの亜種として、$$restPropsという変数もあります
$$propsは自身が受け取ったすべてのプロパティが格納されているのに対し、$$restPropsはexport letで明示的に宣言したプロパティ以外のプロパティのみが格納されます
プロパティを使えば、親コンポーネントから子コンポーネントに対してデータを渡すことができました
Svelteには別に、子のテンプレートの一部を親コンポーネントから自由に指定できる木のがあります。それがスロットです
スロットは <slot> という特別なタグを指定して定義できます
<slot /> が書かれた箇所に、親コンポーネントから自由にコンテンツを配置できます
スロットではHTMLタグだけでなく、コンポーネントや {#if} 等のブロックなど、テンプレートで使える記法を一通り使えます
スロットにコンテンツが挿入されなかった場合に、デフォルトで表示するフォールバックコンテンツを指定することも出来ます
名前付きスロットは、その名の通りスロットに名前が付いたものです。
スロットの名前をタグのslot属性に指定することで、そのタグと子要素がすべてその名前が付いたスロットの部分に挿入されます
slot属性はHTMLタグだけでなく、コンポーネントにも付けられます
<div>で囲みたくない場合は、<svelte:fragment>という特別なタグが使えます
<svelte:fragment>は実際には何のDOM要素にもレンダリングされないため、要素を追加することなく複数の要素をスロットに挿入できます
名前付きスロットは、名前がないスロットとも併用できます
slot属性が付かないコンテンツはすべて名前がないスロットのところに挿入されます
$$slotsは、そのコンポーネントで定義したスロットの名前がキーになったオブジェクトが格納されている特別な定義済み変数です
これを使うことで、あるスロットに親からコンテンツが指定されているかどうかを判断できます
子コンポーネント側から値を渡した場合、スロットプロパティが使えます
各要素を表示する部分がスロットになっていて、親コンポーネントで自由に見た目を変えられる
<List>にlet:プロパティ名={変数}のように書くことで、<slot>から来たスロットプロパティを変数として受け取れます
itemはList.svelte側で<slot>に指定した名前と一致させる必要がある
名前付きスロットでスロットプロパティを使う場合、使う側は少し異なる
Count自体にlet:...を書くのではなく、slot="count"が付いた要素にlet:...を書きます
スロットとプロパティは、親コンポーネントから何かを指定できるという点で似た役割を持つ機能です
使う側にどの程度自由なコンテンツを許すかという基準がある
ボタンの中身を自由に指定できるようにしたいならスロットに、ある程度固定の内容で統一したければプロパティに
JavaScriptには元々addEventListener()などのイベントを扱うAPIがありますが、Svelteではより簡単にイベントを扱うためのショートカットが用意されています
Svelteでは、イベントハンドラをテンプレート中でon:イベント名={関数}のように書くことで指定できます
イベントハンドラはインラインで書くことも可能です
引数にはイベントオブジェクトが渡されます
preventDefault()を呼び出して、<form>要素のデフォルトのフォーム送信の動作をキャンセルしています
イベント修飾子という記法があります。on:イベント名の後に、|で区切って記述します
on:click|preventDefault={handleClick}
イベント修飾子を複数指定することも出来ます。その場合はそれぞれを|で区切ります
Svelteのコンポーネントは、DOMのイベントとは別に独自のイベントを発生させられます
createEventDispatcherを呼び出して、イベントディスパッチャーを作成
dispatch(name, detail)のように呼び出すことで、イベントを発生させられます
nameは任意の文字列で、イベントの名前を表します
detailはイベントの追加情報で、任意のオブジェクトにできます。追加情報が不要な場合は指定しなくても構いません
コンポーネントイベントもDOMイベントと同様にon:イベント名={関数}の形式でイベントハンドラを指定できます
コンポーネントイベントハンドラの第一引数には、CustomEventオブジェクトが渡されます
CustomEventはDOM標準のインターフェースの1つで、イベントに独自のデータを追加できるものです
CustomEventにはdetailというプロパティがあり、dispatchの第2引数に渡した追加情報にアクセスできます
イベント修飾子は、コンポーネントイベントに対しても使えます
TypeScriptを使う場合、createEventDispatcherに型引数を指定できます
const dispatch = createEventDispatcher<{hello: string}();
dispatch('invalid')のように指定されていないイベント名を渡したり、dispatch('hello', 100)のようにdetailの型が違っていたりする場合は型エラーとなり、間違いに気付けます
コンポーネントイベントハンドラ側にも、引数に型を付けられます
function handleHello(event: CustomEvent<string>)
型として使う場合はdetailの型を型引数として受け取れます
detailはstring型を取るため、CustomEvent<string>のように指定しています
子の要素・コンポーネントのイベントを、親のコンポーネントにそのまま転送したいケースがあります
on:click とだけ書くことで、clickイベントを親のコンポーネントに転送できます
DOMイベントを転送する場合、CustomEventオブジェクトではなく、元のオブジェクトのママ転送される
DOMイベントではなくコンポーネントイベントでも同じように転送できます
実行時の処理の流れのことを、コンポーネントのライフサイクルと呼びます
「マウント」とは、コンポーネントがDOMの中に追加されること
「アンマウント」とは、コンポーネントがDOMの中から削除されること
App.svelteは、ページを表示したときに自動的にマウントされ、ページが表示され続ける限りアンマウントされません
「レンダリング」とは、.svelteのテンプレートに書いた内容や内部状態に応じて、DOMの中に実際に要素を追加・更新・削除すること
レンダリングはマウント時に1回行なわれるのと、内部状態を変更したときに都度行われます
ライフサイクル中の特定のタイミングで実行する処理を指定できる、onMount, onDestroy、beforeUpdate、afterUpdateの4つの関数がある
状態が反映されるまで待つ tick を提供している
onMountは、コンポーネントが作成され、初回のレンダリングが終わった直後に呼び出される処理を設定できる関数
onDestroyは、コンポーネントが削除される直前に呼び出される処理を設定できる関数です
コンポーネントが削除されるとき、onDestroyで登録した処理が呼び出され、clearIntervalでタイマーが削除されます。これをしないと、コンポーネントが削除された後もタイマーが呼び出され続け、CPUやメモリを無駄に消費します
beforeUpdateとafterUpdateは、それぞれコンポーネントのDOM構造が更新される直前と直後に呼び出される処理を登録する関数です
tickはSvelteが提供する特殊な関数で、コンポーネント内に保留状態のDOMの更新がある場合に、それが終わるまで待つPromiseを返します
もし保留状態の更新がなければ、Promiseは即座に解決されます
2章長かった…
3章 Svelteのリアクティビティ
データの変化とUIとが同期して更新される性質のことを「リアクティビティ」と呼びます
コンポーネントには、リアクティビティのための機能として、変数に応じてDOMが更新されたり、逆にDOMの変更を変数に反映する仕組みがあります
最も基本的なリアクティビティの仕組みは、変数への代入です
変数への代入を内部状態の変更とみなし、関係するDOM構造へ自動的に反映させます
constキーワードで定義する定数は、再代入ができないためリアクティブの仕組みのためには使えません
+= や -= のような復号代入演算子も、代入と同じく状態の変更として扱われます
ある変数が更新されたとき、それに応じてDOM更新以外の別の処理も行ないたい場合、$: という記法が使えます
$: を最もよく使うケースは、ある変数の変更に応じて別の変数も更新したい場合です
$:の直後に代入分が続く場合は、自動的にlet制限を補ってくれる
$: の後には代入文だけでなく任意の文を書くことができます
$: の後にif文を書くことも出来ます
触ってみた感じ、DOMの更新よりも先に変数の更新処理が走り、$:の処理もDOM更新より先に行なわれそう
変数に加算し、それをそのままDOMに出すだけでも、$:で更に変数の値を書き換えたら、DOMには最終結果だけが出そう
配列の要素を増減したり置き換えたりする関数を使っても、その変更はSvelteから自動的に検出されず、DOM構造も更新されません
DOM更新についても、変数の変化がトリガーになっているということかな?
JavaScriptのスプレッド構文を使うことで、少し簡潔に記述できます
todos = [...todos, '新しいTODO']
配列の再代入のコストは知っておいた方がいい気がする
todos = todos がメモリのコピーなら何の問題もないが、 todos.push で全要素コピーされているなら、スプレッド構文で代入するのとコストは変わらない
ある変数がオブジェクトの場合、そのオブジェクトのプロパティへの代入は、変数への代入と同様に扱われます
代入の左辺に元のオブジェクトの変数が直接現れる場合のみ
間接的な代入は、Svelteからは状態の変更として検出されません
動くようにするには、必ず代入の左辺に変数が直接現れるようにします
<input>要素などのユーザー入力を扱う要素を扱う場合、DOM要素 → コンポーネントの方向でデータが渡せた方が便利な場面があります
バインディングを使うと、属性値がDOM要素側で更新されたとき、それをコンポーネント側の変数にも自動的に反映させられます
バインディングを使うように書き換えるには、value={message}をbind:value={message}に変更します
type="number" や type="range" といった数値を入力する要素では、自動的に数値に変換されたうえで更新される
priceは数値として更新されるため、priceWithTaxの計算のように直接数値の計算に使えます
type="checkbox" では、checked属性をバインディングすることができます
チェックボックスにチェックしているかどうかが真偽値として反映されます
チェックボックスにチェックを入れると、button要素のdisabledがfalseになって送信ボタンが有効化されます
ラジオボタンやチェックボックスは、複数の要素をグループ化して1つの変数に対してバインドすることもできます
ラジオボタンは複数の値から1つだけが選択できるため、バインドする変数は単一の文字列や数値になります
チェックボックスは複数の値が選択可能なため、バインドする変数は配列になります
グループ化してバインドするには特別に bind:group というバインディングを行ないます
value属性に指定する値は文字列以外にすることも可能で、その場合hあそのまま代入されます
select要素へのバインディングにもbind:valueが使えます
グループ化のラジオボタンの例で見たものと似ていますが、select要素にsize変数をバインドしています
select要素はmultiple属性を指定することで複数選択できるセレクトボックスにすることもできます
この場合は、複数選択のため変数は配列になる点に注意
<video> や <audio> といったメディアを扱うDOM要素のバインディング
コンポーネントのプロパティに対しても、バインディングは使えます
thisバインディングは、DOM要素の属性ではなく、DOM要素自体を変数にバインディングすることができる機能
bind:thisを使って、canvasのDOM要素をcanvas変数にバインディングしています
子の変数を通じて、Cavas APIを呼び出せます
本来であれば、DOM描画後に document.getElementById とかで特定していたのを、DOM側から変数に投げ込むことで、JavaScript側が何も気にせずに変数にアクセスできる
これはSvelteのライフサイクルを理解していないと事故りそうやけど、そのあたりがうまく動いている間は脳みそを軽くできるのがいい
Svelteには複数のコンポーネントで状態を共有することを目的とした「ストア」という仕組みがあります
writableストア: 参照・更新の両方が可能なストア
readbleストア: 参照のみが可能なストア
derivedストア: 他のストアの値に応じて値が変化するストア
カスタムストア: svelteが提供する関数以外で作成されたストア
ストアに関係するSvelteの関数は、 svelte/store からインポートして使えます
witableの引数には、そのストアで保持したい値の初期値を渡します
ストアに対しては、以下の3つの操作をすることができます
set(val): ストアの値にvalを設定
subscribe(callback): ストアを購読。ストアの値が変化すると、新しい値を引数にcallbackが実行される。戻り値として、購読を辞める関数を返す
update(updater): 現在の値を引数にupdaterが実行され、その戻り値を新しい値としてストアに設定する
isLogin.subscribe() を呼び出してストアを購読し、ストアに保存されているログイン状態の値が更新されたときに、その値がコンポーネント内部の変数に代入されるようにします
subscribeの戻り値はこのストアの購読を解除する関数で、onDestroyに登録してコンポーネントが破棄される直前に呼び出されるようにしています
subscribeはコールバック関数をいつまでも保持しておく必要があり、わずかながらメモリが無駄になるため、購読の解除は忘れずに行いましょう
handleLogin, handleLogout関数で、それぞれisLogin.set()を呼び出し、ストアの値を更新
Login, Menuの両方からストアを参照し、ログインボタン・ログアウトボタンを押すと、Login、Menu双方が更新される
コンポーネント側にストアの値を受け取る変数が必要になり、購読の解除を忘れないようにする必要があり、やや煩雑
$を使ったストアの自動購読機能
$isLogin とすることで、subscribeが不要になり、onDestroyも不要になる
setも $isLogin = true のようにすることで省略できる
この機能は値の参照しかできないreadableストアには使えない
readbleストアは、writableストアとほぼ同じ機能を持ちますが、値の更新が出来ず参照のみができるストア
直感的には値が変わらないストアなんて何に使うねんって思うけど、どうなんやろ
readable関数は2つの引数を取る
第一引数にはストアの値の初期値
第二引数には「ストアの値の更新を開始し、戻り値として更新を終了する関数を返す」関数を取る
これだけなら、startとかstopっていう関数名は要らない気はする
なお、ここでは説明のためにそれぞれの関数に名前を付けましたが、もちろん無名関数にしても構いません
よかった
readableストアに対しては参照しかできないため、setおよびupdate, $による代入は使えません
一方、subscribeや$による自動購読はwritableストア同様に使えます
derivedストアは、他のストアの値に依存して値が変わるストアです
第一引数には元になるストア、第二引数にはどのようにストアの値を計算するかを関数として渡します
第二引数の関数には、引数としてある時点での元になるストアの値が渡されます
ストア自体じゃなくて、値が渡されるっていうのが大事
第二引数の関数の引数、$currentTimeの$は自動購読という意味ではなく、識別子の一部
「ストアそのもの」ではなく「ストアの値」であることを分かりやすくするための慣習
derivedストアは複数のストアに基づくようなストアも作成することができます
入力した日付と、現在時刻が変わるたびに、表示されるその差が更新されていることを確認できる
svelte/store が提供する writable, readable, derived といった関数を使ってストアを作成してきましたが、あるルールを満たすオブジェクトを作成すれば、これらの関数を使わなくても自前でストアを作成することができます
カスタムストアが有用な場面
サードパーティの状態管理ライブラリをSvelteと組み合わせて使いたい場合
svelte/storeで作成したストアにドメイン固有のインターフェイスを持たせたい場合
カスタムストアの要件として必要なメソッド
subscribe 必須
set
writableストアを使い場合、インデックスをsetあるいはupdateで任意の値に更新できるため、次の課題点があります
配列の範囲外のインデックスも指定できてしまう
画像がループス路湯にインデックスを指定するのがやや煩雑
これらを解消するためにカスタムストアを使ってみます
用途に特化した専用のメソッドが提供されているため、コンポーネント側の記述は簡潔になり、かつ範囲外のインデックスを指定してしまうなどのバグが入りにくいコードになりました
画像の右左がどっちに動くことを期待されているのかぴんと来なくてこんがらがった!
ストア側にドメイン的な記述はしたくないと思いながらも、専用のメソッドを生やせるのはいい
writableストアは各種バインディングの対象として指定できます
input要素のvalue属性に、epochストアをバインドしています
Date型が期待する文字列にならないせいで、思ったような動作はしなかった
set側は期待通り動いたけど、subscribe側が年月日の形式にならなかったせいで、inputで正しく表示されなかった
TypeScriptを使っている場合、writableやreadbleは初期値によって型が推論される
4章 Svelteの高度な機能
{#each}ブロックにはさらに、繰り返しの各要素を厳密に区別するための{#each 配列 as 変数(キー)}という記法が存在します
これをキー付きの{#each}ブロックと呼びます
通常の{#each}ブロックでは、配列の要素数が変化すると、末尾のDOM要素のみが追加または削除されます
Svelteha2番目の<li>要素を削除するのではなく、末尾の<li>要素を削除します。その後、残り2つの<li>要素の動的な部分に新しいデータを反映します。
配列の途中に要素を追加した場合でも同様で、<ul>の末尾に<li>を追加した上で、それぞれの<li>に新しい内容を反映する
{#each}のレンダリングの途中過程では、Svelteは必ずしも状態とDOM要素との関係を1対1では保持しない
アニメーションの機能では、1対1の関係を保持したままDOM要素を更新する必要があるケースがある
キー付きの{#each}ブロックのキー部分に指定した式の結果を使って、状態とDOM要素を1対1関係を保持する
キーはその性質上、配列中の要素を一意に区別できるものを指定する必要がある
{#key}ブロックは、指定した式の値が変化したときに、そのブロック内のコンテンツを再生成するブロック
{#key}で指定したvalueが変わるたびに、トラシジョンが実行されるような使い方
{@...}のように書く記法がいくつかあるが、{#if}のようにブロックを囲うような使い方はしない
{@html 式}は、式の内容をHTMLとしてそのまま出力できる
{@debug 変数}は、変数の値が変化するたびにその値をログに書き出します
ブラウザの開発者ツールを開いている場合は、{@debug}の位置でコードの実行が一時停止して、その時点の状態をデバッグできるようになります
{@debug 変数1, 変数2}のように、複数の変数を指定することも可能
ただし、変数以外は指定できない {@debug obj.foo}、{@debug func(bar)] のように書くことはできない
{@const 変数名 = 式}のように書くことで、テンプレート内で一次変数を定義できる記法
{#each}の中で使って、何かしらの計算をするようなときに使う
reduceは繰り返し要素を計算してくれる関数で、前の計算の結果を持ってくるので合計を計算するのにいい
<svelte:..>のような svelte: から始まる特殊なタグがいくつか存在する
<svelte:self> は自分自身のコンポーネントを表すタグで、このタグを書いた箇所に自分自身が表示される
単純に使うと無限ループになってしまうため、普通は何らかの条件を付けて使う
サンプルの文字列と配列をごちゃ混ぜに入れてる配列、めっちゃ苦手、把握ムズすぎ
<svelte:component>は、テンプレート内で動的にコンポーネントを表示するためのタグ
あらかじめどのコンポーネントを使うか分からないケース、例えばユーザーの入力によって表示するコンポーネントを切り替えたい場合などは、<svelte:component>を使えます
<svelte:component>のthisプロパティに表示したいコンポーネントの変数を渡すことで、そのコンポーネントをその位置にレンダリングすることができます
<svelte:element>はDOM要素を動的に表示できるもの
属性を指定したい場合は、<svelte:element>に直接記載できます
バンディングはbind:thisのみ使えますが、それ以外は使えないため注意してください
<svelte:window>は、windowオブジェクトにイベントハンドラを設定したり、一部のプロパティをバインディングするために使えるタグ
divの中に置いたりは出来ないみたい
コンポーネントの直下に設置する必要がある
<svelte:window>の利点は、このコンポーネントが破棄されるタイミングで自動的にハンドラもwindowオブジェクトから削除される
<svelte:window>は一つのコンポーネントにひとつしか置けないみたい
あんまり使い勝手良くなさそう…?
<svelte:body>はdocument.bodyオブジェクトにイベントハンドラを設定するのに使えるタグ
こちらも<svelte:window>と同じくコンポーネント直下に設置する必要がありそう
<svelte:head>は<head>タグ内のコンテンツを指定できるタグ
コンポーネントの先頭にとりあえず設置するみたいな感じ?分かりやすくてよさそう
<svelte:options>は、コンポーネントごとの設定を変更できるタグ
いくつか設定できるものはあるけど、最初から必要になりそうにはないかな
<svelte:fragment>は、名前付きスロットに複数のコンテンツを渡したい場合にそれらを囲むのに使うことが出来るタグ
menuスロットに<div>で囲んだ要素を渡すと、要素が1つになり横並びにならない
このタグ自体はDOM要素としてはレンダリングされないため、スタイルの都合などで<div>等で囲むのが難しい場合に使うことが出来ます
何かと使えそうではあるけど、頻繁に使うものでもないのかなーという微妙な感じ
<script>ブロック内に書いたJavaScriptは、コンポーネントのインスタンスが生成されるたびに実行されます
一方、<script>にcontext="module"を付けると、その<script>ブロックはモジュールコンテキストになります
モジュールコンテキストは各モジュールにつき1回だけ実行されるようになります
1. コンポーネントの各インスタンス感で共有したい状態があるとき
毎回初期化されたくないってことかな
2. コンポーネント本体以外に、モジュールからエクスポートしたいものがあるとき
こっちはぴんと来ない
どれか1つのビデオを再生した後、別のビデオの再生を開始すると、最初に再生したビデオが停止することが確認できる
モジュールコンテキストによって、複数のインスタンスで現在再生中のビデオが共有できるため
モジュールコンテキストを使うと、コンポーネント本体だけでなく、任意の変数や関数をエクスポートでき、それを他のコードからインポートして使えるようになる
通常のコンテキストでexport letと書いてプロパティを定義しても、外部からインポートできるようにはなりません
通常の<script>ブロックと同様に、モジュールコンテキストもlang="ts"を付けることでTypeScriptで記述できます
TypeScriptの場合、モジュールコンテキストでは定数や関数などのエクスポートに加え、型のエクスポートも可能
コンポーネント側で肩を変更したとしても、App.svelte側は型エラーを検知できない
モジュールコンテキストで export type EventTypes = {hello: string} のように定義し、通常のコンテキストで createEventDispatcher<EventTypes>() のようにすることで、他のコンポーネントからも同様の型制約で関数を使える
高度やから上手く使えるかわからんけど、個人開発でもモノが大きくなったら型をしっかり定義して、後日ソースを変更するときとかにハマらんようにはできそう
DOM要素の動きを制御するための方法として、モーション・トランジション・アニメーションの3つを提供している
モーション: 数値が滑らかに変化するストアを提供する
トランジション: 要素がDOM中に現れるとき、もしくは消えるときの動きを指定できる
アニメーション: DOM中の要素が入れ替わるときの動きをしてできる
モーションは、数値が滑らかに変化するストア
通常のストアは値を設定するとすぐに変化しますが、モーションのストアは値を設定してもすぐにはその値に変化せず、時間をかけて徐々に値が変化します
この値をCSSで使うことで、滑らかな変化のあるUIを実現することが可能になります
svelte/motionにはtweenedとspringの2つの関数がある
tweenedは、値が一定の時間をかけて変化するストアを作成できる関数
tweenedはストアなので、値を使いたいときは $ を付けて購読しないとダメ
デフォルトでは400ミリ秒に設定されています。これを変えるには、第二引数にdurationオプションを渡します
値が変化してる途中で反対のボタンを押すと、途中の状態から次の値に変化していく
すごいこれ
値の変化の「ゆるやかさ」を決める関数をイージング関数と呼びます
デフォルトでは与えられた引数をそのまま返す線形な関数
引数に0~1の範囲の数値を取り、戻り値として同じく0~1の範囲の数値を返す
svelte/easingからさまざまなイージング関数を組み込みで提供している
sineInは最初はゆっくり変化して、徐々に変化が早くなるイージング関数
interpolateはある値から別の値へどのように変化するかを決定する関数
デフォルトでは、数値、Date、配列、オブジェクトが扱える
上記以外を扱いたい場合、自作のinterpolate関数を作る必要がある
自作のinterpolateを作ることで独自の値の変化を扱えるようになる
springは値を変更すると、値が「バネ」のように変化しながら新しい値に落ち着いていくモーション
直接的にdurationやeasingをなどを指定することはできず、パラメータを調整した結果として動きが決まる
トランジションはコンポーネントの状態の変化によってDOM要素が出現するときに、その現れ方をカスタマイズできる仕組み
典型的な例としては、フェードイン・フェードアウトがある
transitionディレクティブには、要素の現れ方や消え方を定義する関数を指定する
トランシジョンは、現れ方と消え方を片方だけ指定したり、別々に指定することもできる
transitionディレクティブの代わりに、inおよびoutディレクティブを使用する
トランシジョンを途中で切り替えた場合、transitionとin/outで挙動が異なる
in/outが同じならtransitionで設定するのが無難そう
アニメーションは要素が移動するときの動きをカスタマイズすることが出来る仕組み
アニメーションはキー付きの{#each}ブロックでのみ使えます
ブロック内の要素の順番が変わるときに、指定したアニメーションが実行される
トランシジョンと異なり、要素が追加・削除されたときは何も起こりません
animationディレクティブを各要素は、{#each}のすぐ内側に書く必要があります
svelte/animateに定義されているのはflip関数のみです
flipは、要素がある位置から別の位置に移動するときに、滑らかに移動させることが出来るアニメーション
すご、自前でやろうと思ったら結構大変層やのに
コンポーネント間でデータを共有できる仕組みとして「ストア」以外に「コンテキスト」がある
コンテキストとは、ある親コンポーネントから、その子コンポーネントに対してデータを共有できる仕組み
ある種のキーバリュー型のデータストアと考えられます
親コンポーネントがsetContextでキーを指定して書き込んだ値を、子コンポーネントがgetContextで同じキーを指定して取り出せます
これらのAPIは必ずコンポーネントの初期化時に呼び出す必要がある
<Point>を<Canvas>の外側で使ってしまうと、getContextがundefinedを返す
getContextを詰悪コンポーネントは必ずついとなるsetContextを呼び出しているコンポーネントの子コンポーネントとして使う必要がある
setContextおよびgetContextのキーを重複しないように管理する必要が出てくる
JavaScriptの新語るを使うことで、常に重複しない一意なキーを作成できる
コンテキストはストアと比較した場合に以下のような特徴を持つ
任意のコンポーネント間でデータをやりとりできない
setContextできるのは親だけで、getContextできるのはその子孫だけ
リアクティブではない
コンテキストの値を変更しても再描画されない
コンポーネントのインスタンス毎に別々の値を保持できる
親子関係が定まっているならコンテキスト、子孫関係にないインスタンス間で同じ値を参照したいならストア
リアクティブ性が必要ならストア
「アクション」はDOM要素に対して、Svelteコンポーネントのようなライフサイクル管理の仕組みを提供するもの
アクションの関数は、第一引数にDOM要素、第二引数にパラメータを受け取り、戻り値としてupdateおよびdestroyをキーにもつオブジェクトを返す
updateにはパラメータが更新されたときに呼び出される関数
destroyにはDOM要素が削除されたときに呼び出される関数をそれぞれ指定します
updateとdestroyは省略可能
アクションをDOMに対して使うには、テンプレート中のHTMLタグでuseディレクティブを使用します
アクションがパラメータを受け取る場合は、関数の更に後に ={params} のようにしてパラメータの指定を続けられる
5章 SvelteKitによる複数ページアプリケーションの開発
余談やけど、ここまでのソースを全部App.svelteに突っ込んでいってるせいで、1000近いコード量になってる
この辺を上手く分ける方法が知りたくて、ちょっと期待してる
Svelteは「ユーザーインターフェースを構築するためのライブラリ」と説明されている
Webアプリケーションと聞いて素朴に思い浮かべるものと比べると、かなり限定的な領域だといえる
SvelteKitは、Webアプリケーションを構築するために必要な、UI以外の部分を提供するフレームワーク
SvelteKitのカバーする範囲は、ルーティング、サーバーサイドロジック、デプロイのためのアダプタなど多岐にわたる
この章を読み始める前に期待してたのとは違いそう!
ここまで適当に書く殴ってきたApp.svelteを一回捨てるために、違うプロジェクトでやり直した方がよさそう
srcフォルダにはアプリケーションのソースコードが格納されます
SvelteKitは「ファイルシステムベースのルーティング」という考え方を採用しています
Webアプリケーションの1つのページは1つのプログラムファイルと対応関係があることから、直感的にルーティングを定義できると考えられている
他方、動的なルーティングの定義が自然にできないなどの制約もある
src/routes以下のフォルダ構成によってルーティングを表現する
src/libに配置されたファイルはルーティングに影響を与えない
「さまざまなページで使用s血合いが、それそのものはページとして表示するにはそぐわない関数・クラス」を定義する
src/libいかにあるファイルは、他のファイルから import '$lib/foobar.js' とすることでインポートできる
階層構造の深い場所にあるファイルから ../../../.. と何度も書く必要がない
staticフォルダには、アプリケーションのロジックとは関係なく、性的に配信したいファイルを配置できる
robots.txtなどのファイルも置くことができる
svelte.config.jsはSvelteとSvelteKitの設定ファイル
vite.config.js はViteの設定を変更できる
SvelteKitプロジェクトは、実際には通常のViteプロジェクト
ファイルシステムベースのルーティングの優れた点は、URLが分かっていれば、それに対応するコードがどこに格納されているか簡単にわかることです
トップページのURLのパスは/なので、src/routes/+page.svelteを見ればいいことになります
4章までで学んできたSvelteコンポーネントとまったく同様の書き方となっている
ページとして扱われるコンポーネントをページコンポーネントと呼ぶこともある
<Counter />コンポーネントが使用されており、src/routes/Counter.svelteにその実装があることが分かります
書籍にはlibにあることになってるけど、実際にはroutes配下にある
このデモアプリには、カウンターの他に、ABOUTとSVERDLEというページがあります
それぞれのページに行き来できますが、ナビゲーションメニューとフッターのテキストは、全てのページに共通していることに気付いた方もいるかもしれません
複数のページに共通する領域やコンテンツを一般にレイアウトと呼ぶ
行き来するたびにナビゲーションメニューやフッターの位置、コンテンツの余白感などが変わってしまってはユーザーにとっては好ましくないので、同じコンテキストを共有するページではこうした共通部分を持つことが一般的です
デモアプリにも src/routes/+layout.svelte としてレイアウトが設置されています
レイアウトも、これまでに学んできた通常のSvelteコンポーネントと変わりありません
「レイアウトが適用される」とは、そのページが表示されるときに、適用されているレイアウトがまずマウントされ、その中の<slot />にページが配置されることを意味する
唯一の違いは、わざわざ自分で書かなくても、SvelteKitあよしなに扱ってくれるという点です
Webアプリに求められる機能や体験が複雑・高度化した結果、JavaScriptなしでは動作しないWebサイトも増えています
一方で、世界の中では高速なインターネットが利用できない場所も珍しくありません
真にアクセシブルなWeb体験には、JavaScriptが利用できない状況への想定が不可欠になりつつあります
SvelteKitではこうした「プログレッシブなWebアプリケーション」と呼ばれる考え方を推進しています
これまではブラウザ上で動作しているだけだった商品ページを、サーバー再度から商品データを取得し、カートへの追加もサーバー再度で保持するようにしたうえで、それらの状態がフロントエンドにも反映されるようにしてみます
npm create svelte@latest svelte-book-kit-tutorial
Demo projectではなくSkeleton projectを選択してください
Sverdleのようなデモ実装が含まれていない空のプロジェクトが作成されます
ここには書かれてないけど、 npm install しとかないといけない
必要なページに対応するルートを作成します
商品ページ: /products/[id]
カートページ: /cart
ここに+page.svelteという名前のファイルを置くと、それがページのコンテンツとして扱われます
第一章、第二小で作成したApp.svelteの内容をコピーします
正しく動作するためにSliderコンポーネントを必要としますので、コピーします
ここまでは、商品ページで表示するための商品のデータを、フロントエンドに直書きしていました
ロードや表示は圧倒的に高速になり利点がありますが、商品データを変更するためにソースコードを変更し、サイト全体をビルドしなおさなければならないという難点もあります
データベースのセットアップやJavaScriptからデータベースにアクセスする処理は後半の章に譲り、データベースからデータを取得しているつもりになって進めていくことにします
App.svelteのコードの部分で定義していた商品データを、サーバサイドでの処理に切り替えていきます
まず、products/[id]/+page.svelte の隣に +page.server.js という名前のファイルを作成します
+page.server.js は、このルートに紐づくサーバー再度で実行する処理を定義するために使います
このファイルがloadという名前の関数をエクスポートしている場合、SvelteKitはこのルートへのアクセスがあった場合に自動的にload関数をサーバーサイドで実行し、その戻り値をdataという名前のプロパティとしてページコンポーネントに渡します
これらの処理は非同期で行われることがほとんどなため、それを模してasync関数で定義しています
SvelteKitではサーバサイドもクライアントサイドもどちらもJavaScriptで書くため、ここでも全く同じコードをコピペして使うことができました
dataという特別な名前のプロパティを使うという規約を除けば、通常のSvelteのコードとまったく違いがないことがわかります
[id]のようなフォルダ名にすることで、load関数から読み取れるように設定できます
load関数の仮引数から分割代入でparamsを受け取れるようにする
仮引数と分割代入って言葉を初めてきいた
オブジェクトで引数を受け取って、それを名前を付けて部分的に取り出す的な感じかな?
他言語でいうところの名前付き引数に近いか
getProductFromDatabaseは、渡されたIDでfilter関数を使って検索するようになっています
find関数で検索してて、getRelatedProductsFromDatabaseがfilter関数を使っている
存在しないIDを指定したら500エラーになるから、そこはどうにかしてほしい
typescriptで fs/promises でwarningが出てるので、 $ npm install --save-dev @types/node しとく
loadProductsがany型を返してしまうので、 const products: Product[] として型宣言しておく
type Product = {} でProduct型の宣言も必要
typescriptでESLint入れたこともあって、エラーを理解してつぶすのに時間がかかるようになってきた
実際のECサイトでは、カートの情報はショップに届いて初めて意味があります
商品ページのカートに入れるボタンを次のように変更します
actionなしでmethodだけ指定したformで囲む
自身へのPOSTか
Nodeで実行されるサーバサイドのコードと、SvelteKitから配信されたスクリプトをブラウザが実行するクライアントサイドのコードは全く別物だという感覚を掴んでください
catch (err: unknown) のあとの err.code で注意されるののを解決できてない
TS18046
それっぽい解決をしたけど、それが正しいのか分からないからあとからもっと調べる
関連商品は「商品の属性によって定まる、その商品と関連した別の商品」と考える
おすすめ商品は「ユーザーの属性と商品の属性に定まる、そのユーザーが購入する見込みの高い別の商品」
前者のデータはload関数で取得して何ら問題ない = ユーザーによって変わるようなものではないから
後者のデータを取得するには比較的計算コストの高い処理を実行する必要がある = ユーザーによって変わるから
load関数によるサーバー再度レンダリングでのデータの取得は、そのデータをレスポンスに載せられる点で優れている
しかしデータ取得処理が完了するまでレスポンスが返せず、ユーザー体験を損なうことがある
おすすめ商品の表示がアプリケーションのユースケースの中で必須であることはあまりなく、なければないでユーザーが目的を達成することを妨げはしないことなので、ページがレンダリングされてから動的に取得することを考える
こうすることで、ユーザーはページの表示を待たされることなく、準備ができた段階でよりリッチな機能にアクセスできるようになる
SvelteKitには「サーバールート」という機能gあある
サーバールートは「人間が見るためのHTMLではなく、機会が読むためのデータ」を配信するルート
サーバールートでは+server.jsを作成する
サーバールートに定義された関数がクライアントサイドで処理されることはなく、必ずサーバーサイドで実行される
サーバールートをまとめたapiというフォルダを作成することが一般的
export async function GET で定義した関数にはGETメソッドでアクセスできる
ページ遷移が完了した段階で、afterNavigateに渡した関数が実行され、fetch関数によって /api/recommends サーバルートが呼び出される
resにはResponseのPromiseが渡ってくるので、これをチェーンしてレスポンスボディのJSONに解決するPromiseに変換し、recommendRequestステートに設定する
テンプレート中で{#await}記法を使ってロード中とロード後の表示を分ける
Promiseが解決された後{:then}ブロックが遠田リングされる
productsにはPromise解決後の値が設定される
recommendRequestの初期値に空っぽのPromiseを渡しておく
loadProductがかぶってるのがきになる
typescript用に type Product = {} を何か所かに書くことになってる
そうでなくても、いくつもESLintに引っかかってる
svelteとtypescriptが会わないんじゃないかと思うくらいあちこちアラートだらけ
コード重複の観点から、product.jsから関数をエクスポートして、+page.server.jsではインポートして使うようにする
さっそく気になってたところを解決
type Product もあわせてexportしちゃおう
typeばっかりあつめたtype.jsみたいなのがあってもいいのかも
入力中のフォームの途中経過やスクロール状態など、ある種のページの状態はページ遷移により破棄され、ブラウザの戻るボタンなどによって元のページに戻ったとしても復元されません
途中経過を保持することがユーザに取って好ましい場合、スナップショット機能を使ってDOMの状態を保持し、再訪問時に復元できる
第6章 Sveltekitリファレンス
SvelteKitでは、Svelteでのコンポーネントよりも少し広い「ページ」という概念を用います
コンポーネントはそれ単体をWebページに埋め込むことも出来れば、他のコンポーネントに埋め込むことも出来ました
SvelteKitでの「ページ」はブラウザで表示するページそのものに直接対応し、ページを他のページやコンポーネントに埋め込めません
ページは「ルーティング」を通して、アプリケーション内のURLと一対一で対応します
ページルートの対となる概念として、サーバルートという概念がある
人間が直接表示することは想定せず、他のページからHTTPリクエストで取得されることを想定しています
表示する内容はHTMLとしてレンダリングできるSvelteコンポーネントではなく、JSONなどのプログラムにより介錯しやすい形式となる
ページがGETかPOSTでしかアクセスできないのに対し、サーバールートはPUT、PATCH、DELETEなどの他のHTTPメソッドでアクセスできます
サーバールート、ページ同様にルーティングを通してURLと一対一で対応付けられます
フォームアクションは、ページとサーバールートの中間にある概念
ページがサーバールートデータを送信するためには、fetchなどを使用してブラウザから独立のHTTPリクエストを送信しなければなりません
JavaScriptを用いなくてもPOSTリクエストwお送信するための仕組みがブラウザには伝統的に備わっている
Webブラウザからアクセス可能となるために、それぞれがURLを割り当てられる必要がある
このURLを割り当てる方法をルーティングと呼ぶ
多数のページから構成されるWebサイトは、一貫したユーザー体験を提供するために、一定の共通部分を持つことがある
これらを効率良く扱うための仕組みとして、SvelteKitは「レイアウト」という概念を提供している
SvelteKitでは多くのコードが「ブラウザで実行されることもあれば、サーバーサイドで実行されることもある」
サーバーサイドでのレンダリングを無効にすることで、アプリケーション全体や、一部のコンポーネントやページを、ブラウザ上でのみ実行されるようにすることもできる
SvelteKitでは、ブラウザサイドの処理もサーバーサイドの処理もJavaScriptで書くことができ、それぞれおをかなり自然に連携できます
一方で、意図しない側でコードが実行されるk遠出不具合を引き起こすこともあります
SvelteKitにけるページは、通常のSvelteのコンポーネントとの大きな違いは次の2つ
Webページを抽象化した概念であるため、通常のSvelteコンポーネントにはない「リクエストの情報を処理する機能」が追加されていること
デフォルトでサーバーサイドレンダリングが有効になっているため、コンポーネント中のコードがサーバーサイドで動作する場合とブラウザ上で動作する場合をある程度意識して書く必要がある
ルーティングフォルダ上に+page.svelteというファイルを置くと、ページルートを追加したことになります
アプリケーションが最初にアクセスを受けた際には、そのURLに対応するルートに割り当てられているページがサーバーサイドでレンダリングされ配信される
ページをクライアントサイドで動作させるために必要なJavaScriptも同時配信され、それ以降の他のページへのアクセスは、原則としてクライアントサイドで高速に処理される
Svelteのコンポーネントの主な役割は、ブラウザ上で何らかのDOM要素にマウントされてから、それ以降の状態を管理し、UIを表示すること
SvelteKitのページは、これに加えて「リクエストの内容によって、コンポーネントの状態を変化させる」役割も担う
+page.svelteと同じフォルダに+page.jsという名前のファイルを置くと、ここに書かれたコードもページのレンダリングに合わせて実行される
ページがサーバーサイドレンダリングされる場合にはサーバーサイドで、ブラウザ上でレンダリングされる場合にはブラウザ上で実行される
+page.server.jsとすることで、常にサーバーサイドで実行されるように設定することができる
ページの表示にデータベースへのアクセエスが必要な場合や、ユーザーからは秘匿したいAPIキーが必要な場合に適しています
+page.server.jsからactionsオブジェクトをエクスポートすると、それに対する+page.svelteでは「フォームアクション」を呼び出せるようになる
ログイン画面など、GETとPOSTをどちらも処理できるルートのばあい、defaultというアクション名を使える
名前付きフォームアクションを定義する場合、デフォルトフォームアクションを定義できない
名前付きフォームアクションは、POSTリクエストに ?/{アクション名} というクエリパラメータを設定して呼び出します
フォームアクションは、ブラウザの典型的な挙動とPOSTリクエストを処理するサーバールートを組み合わせることで、JavaScriptに依存せずWebアプリケーションの最低限の機能を提供するための仕組み
ブラウザが直接サポートする機能だけでは表現したり操作することが難しい複雑な概念を扱う必要がある
直感的に扱うためのUIを開発するためのライブラリがSvelte
状況に応じて可能な限り優れた水準のユーザーインターフェイスを提供できれば理想的です
これを実現する仕組みがプログレッシブなエンハンスメントです
プログレッシブエンハンスメントという概念であり、プログレッシブ と エンハンスメント の直訳をきいても意味ないか
サーバーサイドの処理をフォームアクションで実装していれば、use:enhance という名前のSvelteアクションを設定するだけ
デフォルトのプログレッシブなエンハンスメントは、基本的に「ページの再読み込みを伴わず、ブラウザネイティブのPOSTリクエストの挙動を再現する」ようになっている
カスタムエンハンス関数は、ユーザーが送信ボタンをクリックをした後、実際にブラウザがPOSTリクエストを送出直前に呼び出されます
POSTリクエストが完了した後に処理を実行したい場合、カスタムエンハンス関数の戻り値としてシグネチャの関数を返す
プログレッシブエンハンスを使わずに、formにカスタムのイベントハンドラを登録することで実装する方法もある
on:submitイベントハンドラにpreventDefaultを指定するとともにhandleSubmitを設定しておくことで、ブラウザのフォーム送信機能を無効化し、handleSubmit内でfetchによるPOSTリクエストを創出できる
use:enhanceより冗長ですが、リクエストを細かく制御したい場合に便利
JavaScriptが無効な環境では処理が実行されないため、on:submitの記述が適用されず、ブラウザのフォーム送信機能にフォールバックする
+page.svelte、+page.js、+page.server.jsとは別に、+server.jsをルート定義フォルダに配置することで、ページの配信とは関係のない一般的なHTTPリクエストを処理するルートを定義できます
+server.jsは、GET, POST, PATCH, PUT, DELETEという名前の関数をエクスポートでき、そのルートに届いたHTTPリクエストのリクエストメソッドに応じて該当する関数が呼び出されます
src/routes配下に+から始める特定のファイル名を持つファイルを配置すると、それらが相対パスに対応したルートになる
+page.svelteがある場合にはアクセスできるようになり、+server.jsがある場合にはAPIにアクセスできるようになります
ルートに配置できる特別なファイル名
+page.svelte: ページを配信
+page.js, +page.server.js: load関数やフォームアクションの実装
+server.js: HTTPリクエストを配信
+layout.svelte: 共通レイアウトの適用
+layouyt.js, +layout.server.js: レイアウトにload関数やフォームアクションを実装
+error.svelte: エラーが発生した場合に表示される画面のカスタマイズ
フォルダ名に[id]のように[]で囲まれた文字列を含む場合があると、動的なパラメータを含むルートを定義できる
動的パラメータをページから参照するためには、+page.jsを使ってload関数を実装する必要がある
load関数に渡されるparamsプロパティから、ルート内の動的パラメータにアクセスできる
パラメータを囲む角括弧を二重にすることで、省略可能なパラメータを含むルートを定義できる [[id]]
ルート直下にenやjaなどの表示言語を表すパラメータを指定することでWebサイトの表示言語を設定できるようにするとともに、その言語設定を省略することもできるようにしたい場合には、/[[lang]]/home/about/+page.svelte などとして省略可能な言語設定を含むルートを定義できる
フォルダ名に [...rest] のような文字列を書くことで、残余パラメータを含むルートを定義できる
残余パラメータは、その後ろ全てにマッチって感じ
そこにスラッシュが含まれていても関係なし
ルーティング定義中で(グループ名)のように丸括弧で囲まれたセグメントを定義すると、そのセグメントは最終的なルーティング定義に影響を与えず、ルーティング定義の階層構造にのみ影響するようになる
レイアウトの適用範囲の制御に利用できる
ルーティングのパターンマッチでは、マッチャーと呼ばれる関数を定義する必要がある
src/params/langcode.js といったファイルを作成し、文字列を受け取り真偽値を返すmatchという関数をエクスポートする必要がある
[lang=langcode]/home/+page.svelte のようにすることで、マッチャーを指定できる
ルートの解決はサーバーサイドでもブラウザ上でも実行される可能性があるので、データベースにアクセスして判断するなどはできない
ルートマッチの優先順位は下記で決定される
具体的な定義を持つルートが優先される。パラメータを持たないルートと持つルートだと、持たないルートが優先
パターンマッチを含むパラメータは、そうでないパラメータよりも優先される
同着の場合はアルファベット順で優先順位が定まる
ファイル名やフォルダ名に使えない文字列はエスケープする必要がある
エスケープは、[x+2f]のように表現する。[]の中に、x+に続けて表現したい文字の文字コードを16新数で表記します
レイアウトを使うと、複数のページに共通するレイアウトを適用できる
SvelteKitは+layout.svelteという名前のファイルがあれば、そのファイルと同じかそれ以下のフォルダのページをレンダリングする際に、まずそのレイアウトをレンダリングし、その中の<slot>要素にページとなるコンポーネントをマウントしてレンダリングするようになる
レイアウトはネストすることができる
レイアウトはな階層でもネストすることが出来ますが、シンプルなレイアウトを使いたい場合、中間の階層構造を無視してより上層のレイアウトを採用できます
+page.svelteまたは、+layout.svelteのファイル名に、@に続けてレイアウトを採用したいルートのセグメントを記述します
SvelteKitのフレームワークレベルでの挙動に手を加えるために、いくつかのフックが用意されています
フックは、src/hooks.server.jsまたはsrc/hooks.client.jsに所定の名前の関数をエクスポートすることで実装出来、SvelteKitはそれぞれを所定のタイミングで実行してくれます
handle関数は、アプリケーションがリクエストを受け取ったときに、毎回実行されます
最もシンプルな使い方は、ゼロから全く新しいレスポンスを生成して返す方法です
新しいレスポンスを返した場合、ルーティング定義したコードは一切実行されず、ハンドラの中で処理が完結する
resolve関数にevent変数を渡して呼び出すと、通常のSvelteKitのルーティングとレンダリングが実行される
リクエストやレスポンスを一部だけ改変することもできる
クッキーを設定したりすることに使うと便利
handleFetch関数は、ページload関数とフォームアクションのaction関数がサーバーサイドで実行され、その中でSvelteKitが提供するfetch関数が呼び出されたときに実行される
典型的な使用方法は、ゲートウェイを介して公開されているAPIサーバーへのリクエストのショートカット
load関数や、action関数は、サーバーサイドでも実行されることもクライアントサイドで実行されることもある
handleError関数はロード中やレンダリング中に予期しないエラーが発生したときに呼び出され、errorとeventを受け取る
予期しないエラーの発生を統一的に検知してログを記録するほか、ユーザーに対して表示するエラーメッセージを、より親切でかつセンシティブな情報を含まない内容に加工できる
$app/environment 実行環境に関する情報
$app/forms フォームアクションに関するヘルパー
$app/navifation ページ移動に関するヘルパー
$app/paths パスに関するヘルパー
$app/stores アプリケーションレベルのストア
ハイドレーションとは、JavaScriptによる動的な処理を含まないHTMLを、動的なHTMLにする過程のことをいう
@sveltejs/kit load関数やサーバールートで使うユーティリティ関数を提供するモジュール
$env 環境変数へのアクセスを提供するモジュール
SvelteKitアプリケーションのビルドは、次の二段階に渡って実行される
Viteによるアプリケーションコードの生成
アダプターによるデプロイ先の環境への適合化
新規に作成したSvelteKitプロジェクトには、デフォルトでadapter-autoアダプターが設定されている
一部のホスティングサービスを使うのであれば、ビルドやアダプターの設定はほとんど要らず、GitHubなどのリポジトリのホスティング環境と連携設定するだけでデプロイを完了させられる
各サービス固有の設定をしたい場合は、実装アダプターを設定し、svelte.config.jsで設定する必要がある
各アダプターで使用できる設定の詳細は頻繁に更新されるため、公式ドキュメントを参照するようにする
adapter-autoが対応していない環境でSvelteKitアプリケーションをホストする場合は、adapter-nodeを使用してNodeサーバーとして起動できるようにビルドする必要がある
一般的なレンタルサーバーやVPSでのホスティングにもこのアダプターを使うことになる
$ npm install -D @sveltejs/adapter-node でインストールする
$ npm run build ビルド
$ node build ビルド成果物の起動
$npm ci --prod 本番環境の依存関係のインストール
SveltekItは性的サイトを生成するために、adapter-staticを使用できる
アプリケーションをプリレンダリングし、静的ページの集合としてレンダリングする
第7章 MongoDBとVercelによる本番環境の構築
開発そのものに集中できるように、環境の準備については積極的に既存サービスを利用して省力化する方針
一応自前で作れるようにしたいから、Dockerで用意してみようとおもう
MongoDB Atlas: データベースサーバーに利用
GitHub: ソースコードの外部リポジトリに利用
Vercel: 公開用サーバーに利用
Auth0: ログイン機能の実装に利用
まずはWebサーバーとデータベースは作る
認証はどうしようかな
GitHubもどうしようかなー
くらいな感じ
Webサービスとして提供して他の人にも使ってもらいたい場合は、インターネットに公開されたサーバーを用意し、そのうえでアプリケーションを実行する必要がある
後悔することを意図したサーバーやそれに付随する環境のことを本番環境と呼びます
SvelteKitで作成したアプリケーションは、ソースコードをそのままサーバーに持って行っただけでは動作しない
アプリケーションのソースコードを実行に適した以下の2種類のコードに変換する必要がある
サーバーで実行されるJavaScriptコード
クライアントで実行されるJavaScriptコード
動作する形のコードに変換する過程をビルドと言います
ある時点でのアプリケーションを本番環境に適用する作業を「デプロイ」と呼ぶ
必要なデプロイ作業はアプリケーションや本番環境によって異なりますが、SvelteKitの場合は以下のような作業が必要
ソースコードの取得
ビルド
ビルドで生成されたコードをサーバーに反映
VercelはSvelteKitをサポートしており、ビルドを自動的に行うほか、コードを配信・実行するサーバーを自動的に用意し、それらにビルド結果のコードを反映します
dockerで動かすなら、コンテナ起動時にrun devを実行すればいいだけな気はしてる
商品やカートなどのデータは、アプリケーションが動くコンピューターに直接JSONファイルとして保存していた
サーバーでも同じようにしようとすると、以下のような課題がある
課題1: サーバーによっては、任意にファイルを作成・保存できなかったり、作成できて一定期間で消されたりすることがある
課題2: 負荷を分散するために複数台のサーバーを用意すると、それらの間でどうやって情報を共有するかが課題
データがどこかのタイミングで破棄されてしまう、複数のServerlessFunctionsでデータを共有できないという課題がある
このような課題を回避するため、本番環境では「データベース」と呼ばれるサーバーを別途用意することが多い
MongoDBへ接続・操作するためのライブラリであるmongodbパッケージをインストールする
$ npm install mongodb@"^5.0.0"
現時点でのmongodbのバージョンが7なので、そのあたりは確認して調整
$ npm install mongodb
"mongodb": "^6.8.0"
6.8.0が入った
.envはGitにコミットされません
svelteの.gitignoreに最初から.envは除外されるように書かれている
MongoDBサーバーへの接続情報をMONGODB_URIというキーで記載しています
直接コードには記載せず.envに記載するのは、接続情報が開発環境と本番環境で異なるためと、接続情報はそれ自体が機密情報であるため
公開リポジトリにプッシュしてしまうと、その接続情報を使ってあなたのMongoDBサーバーで誰でもアクセスできてしまう
インストールしたmongodbパッケージから、MongoClientをインポートします
これを使うことで、Node.jsからMongoDBサーバーへ接続し、各種操作ができるようになります
$env/dynamic/private は、SvelteKitが提供する特別なモジュール
インポートしたenvを経由して、環境変数や.envに記載した値を読み込めます
$env/dynamic/private に private と付いているのは、クライアントサイドには公開されない変数のみを読み込むため
このモジュールをクライアントサイドのコードでインポートしようとするとエラーとなります
他に、$env/dynamic/public、$env/static/private、$env/static/public のバリエーションが存在する
実行時にサーバーサイドでのみ参照するため、dynamic/private を使っています
.envはコミットされないので、別の環境では別途MONGODB_URIを用意する必要があります
MONGODB_URIの後に ?? "mongodb://dummy" と続いているのは、環境変数にMONGODB_URIが指定されていなくてもビルドに失敗しないようにするため
ビルド時はMongoDBには接続する必要がないため、ダミーの接続情報を指定しています
あらかじめ有効な設定ができる場合は、ダミーの部分は不要です
docker-composeで動かそうとすると少し調整が必要だった
DB操作まで学べたので、いったん中断
作りたいものを作ってみて、分からないことがあったら戻ってくるくらいのつもりで。
更新履歴
2024/08/22 かきおわり
2024/03/05 かきはじめ